﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.Linq;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Net;
using ScrumTable.DL.Data.Generic.Properties;

#endregion


namespace ScrumTable.DL.Data.Generic.Cache
{
    /// <summary>
    /// This class wraps a team foundation project instance.
    /// </summary>
    public class CacheProject : CacheElementBase
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private CacheWorkItemType _userStoryType;
        private CacheWorkItemType _taskType;
        private CacheWorkItemType _bugType;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the owning project instance.
        /// </summary>
        public override CacheProject Project { get { return this; } }

        /// <summary>
        /// Gets the project portal (e.g. sharepoint server) uri. The value
        /// is only available if you specified a valid project.
        /// </summary>
        /// <remarks>Returns the retrieved sharepoint server url or a null reference.</remarks>
        public ServiceRegistration ProjectPortal
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the reporting protal (e.g. report server) uri. The value is only available if you
        /// specified a valid project.
        /// </summary>
        /// <remarks>Returns the retrieved reporting server url or a null reference.</remarks>
        public ServiceRegistration ReportingPortal
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the associated project members.
        /// </summary>
        public CacheElementList<CacheMember> Members
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the associated project teams.
        /// </summary>
        public CacheElementList<CacheTeam> Teams
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the associated reports.
        /// </summary>
        public CacheElementList<CacheReport> Reports
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the underlying user story workitem type.
        /// </summary>
        public CacheWorkItemType UserStoryType
        {
            get
            {
                if (_userStoryType == null)
                {
                    _userStoryType = WorkItemTypes.TryGetByName(Project.OwnerContext.Schema.UserStorySchema.SchemaMappedName);
                }
                return _userStoryType;
            }
        }

        /// <summary>
        /// Gets the underlying task workitem type.
        /// </summary>
        public CacheWorkItemType TaskType
        {
            get
            {
                if (_taskType == null)
                {
                    _taskType = WorkItemTypes.TryGetByName(Project.OwnerContext.Schema.TaskSchema.SchemaMappedName);
                }
                return _taskType;
            }
        }

        /// <summary>
        /// Gets the underlying bug workitem type.
        /// </summary>
        public CacheWorkItemType BugType
        {
            get
            {
                if (_bugType == null)
                {
                    _bugType = WorkItemTypes.TryGetByName(Project.OwnerContext.Schema.BugSchema.SchemaMappedName);
                }
                return _bugType;
            }
        }

        /// <summary>
        /// Gets the list of child interations.
        /// </summary>
        public CacheElementList<CacheIteration> Iterations
        {
            get;
            private set;
        }

        /// <summary>
        /// Geths the storage with the workitems.
        /// </summary>
        public CacheWorkItemStore WorkItems { get; private set; }

        /// <summary>
        /// Gets the work item types project members.
        /// </summary>
        public CacheElementList<CacheWorkItemType> WorkItemTypes
        {
            get;
            private set;
        }

        /// <summary>
        /// Returns/Sets the last save time of the data changes.
        /// DateTime.MinValue means that the project has never been saved.
        /// </summary>
        public DateTime LastSaveTime { get; private set; }

        /// <summary>
        /// Contains true if the current project has been filled with data.
        /// </summary>
        internal bool IsDataImported { get; private set; }


        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new CacheProject instance. This constructor is used in
        /// the current library only.
        /// </summary>
        internal CacheProject(
            string unqiueId,
            int id,
            string name,
            ServiceRegistration projectPortal,
            ServiceRegistration reportingPortal,
            IEnumerable<CacheTeam> teams,
            IEnumerable<CacheMember> members,
            IEnumerable<CacheWorkItemType> workItemTypes)
            : base(unqiueId, id, name, CacheIdentity.DefaultRevision)
        {
            PreCondition.AssertNotNull(projectPortal, "projectPortal");
            PreCondition.AssertNotNull(reportingPortal, "reportingPortal");

            LastSaveTime = DateTime.MinValue;
            ProjectPortal = projectPortal;
            ReportingPortal = reportingPortal;
            Teams = new CacheElementList<CacheTeam>(teams, this);
            Members = new CacheElementList<CacheMember>(members, this);
            WorkItems = new CacheWorkItemStore(null, this);
            WorkItemTypes = new CacheElementList<CacheWorkItemType>(workItemTypes, this);
            Iterations = new CacheElementList<CacheIteration>(null, this);
            Reports = new CacheElementList<CacheReport>(null, this);
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Clones the current and all identities below the current cache element
        /// and fills them into the target store.
        /// </summary>
        /// <param name="target">Specifies the store which should be filled with the identities.</param>
        public override void CloneIdentities(CacheIdentityStore target)
        {
            base.CloneIdentities(target);

            Teams.CloneIdentities(target);
            Members.CloneIdentities(target);
            WorkItems.CloneIdentities(target);
            Iterations.CloneIdentities(target);
        }

        /// <summary>
        /// Imports the iteration and work item data into the current cache project.
        /// </summary>
        /// <param name="iterations">Specifies the iterations to import.</param>
        /// <param name="reports">Specifies the reports to import.</param>
        /// <param name="workItemPool">Specifies the pool of work items to import.</param>
        /// <remarks>The import avoids value tracking.</remarks>
        public void ImportData(IList<CacheIteration> iterations, IList<CacheReport> reports, IDictionary<int, CacheWorkItem> workItemPool)
        {
            Iterations = new CacheElementList<CacheIteration>(iterations, this);
            Reports = new CacheElementList<CacheReport>(reports, this);
            WorkItems = new CacheWorkItemStore(workItemPool, this);
            IsDataImported = true;
        }

        /// <summary>
        /// Checks the cache data structures for changes and lists them
        /// in the CacheDataChanges instance.
        /// </summary>
        /// <returns>Returns the changes as a new instance.</returns>
        internal CacheDataChanges GetChanges()
        {
            CacheIdentityStore cacheIdentities = new CacheIdentityStore();
            CloneIdentities(cacheIdentities);

            return new CacheDataChanges(
                this,
                Iterations.ExtractChangedBuckets(),
                WorkItems.ExtractChangedBuckets(),
                cacheIdentities);
        }

        /// <summary>
        /// Accepts (or rejects) the pending changes on cache level.
        /// </summary>
        /// <param name="changes">Specifies the data changes to accept.</param>
        /// <remarks>Merges back values from parallel thread.</remarks>
        internal void MergeChanges(CacheDataChanges changes)
        {
            PreCondition.AssertNotNull(changes, "changes");

            // update last save time field
            if (changes.LastSaveTime >= LastSaveTime)
            {
                LastSaveTime = changes.LastSaveTime;
            }

            // sync changes from back-end system
            if (changes.IterationsToSync.Count != 0)
            {
                Iterations.MergeChanges(
                    from iterSavePair in changes.IterationsToSync
                    select new CacheElementMergeInfo<CacheIteration>(iterSavePair.Value.Bucket, iterSavePair.Value));
            }
            if (changes.WorkItemsToSync.Count != 0)
            {
                WorkItems.MergeChanges(
                    from wiSavePair in changes.WorkItemsToSync
                    select new CacheElementMergeInfo<CacheWorkItem>(wiSavePair.Value.Bucket, wiSavePair.Value));
            }
        }
        
        /// <summary>
        /// Retrieves all WorkItems (user stories) stored in the product backlog. 
        /// </summary>
        /// <returns>Returns the evaluated user stories.</returns>
        internal ICollection<CacheWorkItem> GetProductBacklog()
        {
            using (new TimeLogger<CacheProject>(true, "GetProductBacklog"))
            {
                return WorkItems.GetChildren(new ValueChain<string>(UniqueId), UserStoryType);
            }
        }

        /// <summary>
        /// Retrieves all WorkItems (user stories) stored in the iteration (sprint) backlog. 
        /// </summary>
        /// <returns>Returns the evaluated user stories.</returns>
        internal ICollection<CacheWorkItem> GetSprintBacklog(CacheIteration iteration)
        {
            using (new TimeLogger<CacheProject>(true, "GetSprintBacklog"))
            {
                return WorkItems.GetChildren(new ValueChain<string>(UniqueId, iteration.UniqueId), UserStoryType);
            }
        }

        
        /// <summary>
        /// Gets all user stories below the project or iteration.
        /// </summary>
        /// <param name="parentIdChain">Specifies the parent id chain.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal ICollection<CacheWorkItem> GetUserStories(ValueChain<string> parentIdChain)
        {
            using (new TimeLogger<CacheProject>(true, "GetUserStories"))
            {
                return WorkItems.GetChildren(parentIdChain, UserStoryType);
            }
        }

        /// <summary>
        /// Retrieves a work item below the given id chain.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id of the work item above.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal CacheWorkItem TryGetWorkItem(ValueChain<string> parentIdChain)
        {
            using (new TimeLogger<CacheProject>(true, "TryGetWorkItem"))
            {
                return WorkItems.TryGetChild(parentIdChain.ParentChain, parentIdChain.Top);
            }
        }
        
        /// <summary>
        /// Gets all tasks below the user story with the given id.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id of the user story above.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal ICollection<CacheWorkItem> GetTasks(ValueChain<string> parentIdChain)
        {
            using (new TimeLogger<CacheProject>(true, "GetTasks"))
            {
                return WorkItems.GetChildren(parentIdChain, TaskType);
            }
        }

        /// <summary>
        /// Gets all bugs below the project/iteration/user story with the given id.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id of the user story above.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal ICollection<CacheWorkItem> GetBugs(ValueChain<string> parentIdChain)
        {
            using (new TimeLogger<CacheProject>(true, "GetBugs"))
            {
                return WorkItems.GetChildren(parentIdChain, BugType);
            }
        }

        /// <summary>
        /// Gets the user story with the given id.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id chain of the parent objects.</param>
        /// <param name="userStoryId">Specifies the id of the user story.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal CacheWorkItem GetUserStory(ValueChain<string> parentIdChain, string userStoryId)
        {
            using (new TimeLogger<CacheProject>(true, "GetUserStory"))
            {
                return WorkItems.TryGetChild(parentIdChain, userStoryId, UserStoryType);
            }
        }

        /// <summary>
        /// Gets the task with the given id below the given project and iteration.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id chain of the parent objects.</param>
        /// <param name="taskId">Specifies the id of the task.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal CacheWorkItem GetTask(ValueChain<string> parentIdChain, string taskId)
        {
            using (new TimeLogger<CacheProject>(true, "GetTask"))
            {
                return WorkItems.TryGetChild(parentIdChain, taskId, TaskType);
            }
        }

        /// <summary>
        /// Gets the bug with the given id below the given project and iteration.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id chain of the parent objects.</param>
        /// <param name="bugId">Specifies the id of the bug.</param>
        /// <returns>Returns the found work item or a null reference, if it could not be retrieved.</returns>
        internal CacheWorkItem GetBug(ValueChain<string> parentIdChain, string bugId)
        {
            using (new TimeLogger<CacheProject>(true, "GetBug"))
            {
                return WorkItems.TryGetChild(parentIdChain, bugId, BugType);
            }
        }

        /// <summary>
        /// Creates a new attachment and associates it with the specified work item object.
        /// </summary>
        /// <returns>Returns the created attachment object.</returns>
        internal CacheAttachment CreateAttachment(ValueChain<string> parentIdChain)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            using (new TimeLogger<CacheProject>(true, "CreateAttachment"))
            {
                CacheWorkItem workItem = TryGetWorkItem(parentIdChain);

                if (workItem == null)
                    throw new ArgumentException(string.Format(Resources.ExcCacheProjWorkItemNotFound, parentIdChain));

                CacheAttachment atta = OwnerContext.ElementFactory.CreateAttachment(
                    Resources.DatTfsProjCreateNewAttachment,
                    CacheIdentity.DefaultId,
                    string.Empty,
                    null);
                
                workItem.Attachments.Add(atta);
                return atta;
            }
        }

        /// <summary>
        /// Creates a new task and associates it with the specified parent iteration / project
        /// and user story.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id of the user story.</param>
        /// <returns>Returns the created task work item object.</returns>
        internal CacheWorkItem CreateTask(ValueChain<string> parentIdChain)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            using (new TimeLogger<CacheProject>(true, "CreateTask"))
            {
                // create work item
                CacheWorkItem task = OwnerContext.ElementFactory.CreateWorkItem(TaskType);
                task.Name = Resources.DatTfsProjCreateNewTask;
                task.Iteration = Iterations.TryGetById(parentIdChain.SubChain(1).Bottom);

                // add to cache
                WorkItems.Add(parentIdChain, task);

                // link task to parent user story
                CacheWorkItem userStory = GetUserStory(parentIdChain.ParentChain, parentIdChain.Top);
                
                if (userStory != null)
                {
                    userStory.ChildLinks.Add(OwnerContext.ElementFactory.CreateWorkItemLink(task.Id, task.UniqueId));
                }
                return task;
            }
        }

        /// <summary>
        /// Creates a new task and associates it with the specified parent iteration / project
        /// and user story.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id of the parent object.</param>
        /// <returns>Returns the created task work item object.</returns>
        internal CacheWorkItem CreateBug(ValueChain<string> parentIdChain)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            using (new TimeLogger<CacheProject>(true, "CreateBug"))
            {
                // create work item
                CacheWorkItem bug = OwnerContext.ElementFactory.CreateWorkItem(BugType);
                bug.Name = Resources.DatTfsProjCreateNewBug;
                bug.Iteration = Iterations.TryGetById(parentIdChain.SubChain(1).Bottom);
                
                // add to cache
                WorkItems.Add(parentIdChain, bug);

                // link bug to parent user story, if a parent user story exists
                CacheWorkItem userStory = GetUserStory(parentIdChain.ParentChain, parentIdChain.Top);

                if (userStory != null)
                {
                    userStory.ChildLinks.Add(OwnerContext.ElementFactory.CreateWorkItemLink(bug.Id, bug.UniqueId));
                }
                return bug;
            }
        }

        /// <summary>
        /// Moves an existing task and associates it with the specified parent user story.
        /// </summary>
        /// <param name="toMove">Specifies the task, which should be moved.</param>
        /// <param name="oldUserStoryId">Specifies the old parent user story id.</param>
        /// <param name="newUserStoryId">Specifies the new parent user story id.</param>
        /// <returns>Returns the created task work item object.</returns>
        internal void MoveTask(CacheWorkItem toMove, ValueChain<string> oldUserStoryId, ValueChain<string> newUserStoryId)
        {
            using (new TimeLogger<CacheProject>(true, "MoveTask"))
            {
                PreCondition.AssertNotNull(toMove, "toMove");
                CacheWorkItem oldUserStory = GetUserStory(oldUserStoryId.ParentChain, oldUserStoryId.Top);
                CacheWorkItem parentUserStory = GetUserStory(newUserStoryId.ParentChain, newUserStoryId.Top);

                // delete links on both sides, old user story and current task
                oldUserStory.ChildLinks.Remove(OwnerContext.ElementFactory.CreateWorkItemLink(toMove.Id, toMove.UniqueId));

                // update iteration path
                toMove.Iteration = parentUserStory.Iteration;

                // create links to new parent/child
                parentUserStory.ChildLinks.Add(OwnerContext.ElementFactory.CreateWorkItemLink(toMove.Id, toMove.UniqueId));
            }
        }

        /// <summary>
        /// Moves an existing user story and associates it with the specified parent iteration.
        /// </summary>
        /// <param name="toMove">Specifies the story, which should be moved.</param>
        /// <param name="oldParentIdChain">Specifies the old parent id.</param>
        /// <param name="newParentIdChain">Specifies the new parent id.</param>
        internal void MoveUserStory(CacheWorkItem toMove, ValueChain<string> oldParentIdChain, ValueChain<string> newParentIdChain)
        {
            using (new TimeLogger<CacheProject>(true, "MoveUserStory"))
            {
                PreCondition.AssertNotNull(toMove, "toMove");

                ICollection<CacheWorkItem> childTasks = GetTasks(new ValueChain<string>(oldParentIdChain, toMove.UniqueId));
                toMove.Iteration = Iterations.TryGetById(newParentIdChain.SubChain(1).Bottom);

                foreach (CacheWorkItem task in childTasks)
                {
                    task.Iteration = toMove.Iteration;
                }
            }
        }

        /// <summary>
        /// Moves an existing bug and associates it with the specified parent item this may be a user story, an iteration or the project.
        /// </summary>
        /// <param name="toMove">Specifies the story, which should be moved.</param>
        /// <param name="oldParentIdChain">Specifies the old parent id.</param>
        /// <param name="newParentIdChain">Specifies the new parent id.</param>
        internal void MoveBug(CacheWorkItem toMove, ValueChain<string> oldParentIdChain, ValueChain<string> newParentIdChain)
        {
            using (new TimeLogger<CacheProject>(true, "MoveBug"))
            {
                PreCondition.AssertNotNull(toMove, "toMove");

                CacheWorkItem oldUserStory = GetUserStory(oldParentIdChain.ParentChain, oldParentIdChain.Top);
                CacheWorkItem parentUserStory = GetUserStory(newParentIdChain.ParentChain, newParentIdChain.Top);

                // delete links on both sides, old user story and current task
                if (oldUserStory != null)
                {
                    oldUserStory.ChildLinks.Remove(OwnerContext.ElementFactory.CreateWorkItemLink(toMove.Id, toMove.UniqueId));
                }

                // move iteration
                toMove.Iteration = Iterations.TryGetById(newParentIdChain.SubChain(1).Bottom);

                // create links to new parent/child
                if (parentUserStory != null)
                {
                    parentUserStory.ChildLinks.Add(OwnerContext.ElementFactory.CreateWorkItemLink(toMove.Id, toMove.UniqueId));
                }
            }
        }

        /// <summary>
        /// Creates a new user story and associates it with the specified parent iteration / project.
        /// </summary>
        /// <param name="parentIdChain">Specifies the id of the parent element.</param>
        /// <returns>Returns the created user story work item object.</returns>
        internal CacheWorkItem CreateUserStory(ValueChain<string> parentIdChain)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            using (new TimeLogger<CacheProject>(true, "CreateUserStory"))
            {
                // create work item
                CacheWorkItem userStory = OwnerContext.ElementFactory.CreateWorkItem(UserStoryType);
                userStory.Iteration = Iterations.TryGetById(parentIdChain.SubChain(1).Bottom);
                userStory.Name = Resources.DatTfsProjCreateNewUserStory;
                
                WorkItems.Add(parentIdChain, userStory);
                return userStory;
            }
        }

        /// <summary>
        /// Validates the current project. If it some required parts (such as WorkItem types) are missing,
        /// this procedure throw an exception.
        /// </summary>
        internal void Validate()
        {
            if (UserStoryType == null) { throw new CacheMissingWorkItemTypeException(OwnerContext.Schema.UserStorySchema.SchemaMappedName); }
            if (BugType == null) { throw new CacheMissingWorkItemTypeException(OwnerContext.Schema.BugSchema.SchemaMappedName); }
            if (TaskType == null) { throw new CacheMissingWorkItemTypeException(OwnerContext.Schema.TaskSchema.SchemaMappedName); }
        }

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            base.ReleaseManagedResources();

            Teams.ForEach(team => team.Dispose());
            Members.ForEach(member => member.Dispose());
            Iterations.ForEach(iteration => iteration.Dispose());
            WorkItems.ForEach(workItem => workItem.Dispose());
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion
    }
}